import sys
import os
import datetime
import json
import getpass

actions = ['create', 'update', 'show', 'delete', 'copy']
dtypes = ['raw', 'train', 'test']
timeFmt = '%Y-%m-%d %H:%M:%S'
DATATYPE = {"RAW_DATA": 1000,
            "TRAIN_DATA": 1001,
            "TEST_DATA": 1002}
RE_DATATYPE = {v: k for k, v in DATATYPE.items()}


class DottableDict(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self.__dict__ = self

    def allowDotting(self, state=True):
        if state:
            self.__dict__ = self
        else:
            self.__dict__ = dict()


def get_user():
    return getpass.getuser()


class Metadata:
    def __init__(self, dataType=DATATYPE['RAW_DATA']):
        super().__init__()
        self.dataType = dataType
        self.metadata = {'name': '', 'creator': '', 'createDate': datetime.datetime.now().strftime(timeFmt),
                         'instruments': '', 'location': '',
                         'sets': {'format': '', 'type': RE_DATATYPE[self.dataType], 'origin': '', 'comments': '',
                                  'history': [{
                                      'updated': datetime.datetime.now().strftime(
                                          timeFmt),
                                      'creator': '', 'changes': 'Initialize metadata.'}]},
                         'comments': ''}

    def add_attributes(self, newAttributes):
        try:
            self.metadata.update(newAttributes)
        except Exception as e:
            assert e

    def load_metadata(self, metadata, dataType=DATATYPE['RAW_DATA'], isCheck=True):
        if isCheck and self.check_metadata(metadata, dataType):
                self.metadata = metadata
                self.dataType = dataType
        elif isCheck == False:
            self.metadata = metadata
            if self.metadata.get('sets') and self.metadata['sets'].get('type') in DATATYPE.keys():
                self.dataType = DATATYPE[self.metadata['sets']['type']]
        else:
            raise Exception("Metadata format exception!")

    @staticmethod
    def check_metadata(metadata, dataType=DATATYPE['RAW_DATA']):
        if dataType == DATATYPE['RAW_DATA']:
            if metadata.get('name') and metadata.get('creator') and metadata.get('location') and metadata.get('sets'):
                if metadata['sets'].get('type') and metadata['sets'].get('history') and metadata['sets'].get('format'):
                    if metadata['sets']['type'] == 'RAW_DATA':
                        return True
        if dataType == DATATYPE['TRAIN_DATA']:
            if metadata.get('name') and metadata.get('creator') and metadata.get('location') and metadata.get('sets'):
                if metadata['sets'].get('type') and metadata['sets'].get('history') \
                        and metadata['sets'].get('origin') and metadata['sets'].get('format'):
                    if metadata['sets']['type'] == 'TRAIN_DATA':
                        return True
        if dataType == DATATYPE['TEST_DATA']:
            if metadata.get('name') and metadata.get('creator') and metadata.get('location') and metadata.get('sets'):
                if metadata['sets'].get('type') and metadata['sets'].get('history') \
                        and metadata['sets'].get('origin') and metadata['sets'].get('format'):
                    if metadata['sets']['type'] == 'TEST_DATA':
                        return True
        return False

    def add_history(self, creator, changes):
        aHist = {"updated": datetime.datetime.now().strftime(timeFmt), "creator": creator, "changes": changes}
        self.metadata['sets']['history'].append(aHist)

    def merge_raw(self, rawMetadata):
        if self.check_metadata(rawMetadata, dataType=DATATYPE['RAW_DATA']):
            self.metadata['instruments'] = rawMetadata['instruments']
            self.metadata['sets']['origin'] = rawMetadata['location']
        else:
            raise Exception("Check raw metadata failed, maybe raw metadata has unexpected information.")

    def __str__(self):
        return json.dumps(self.metadata, indent=4, separators=(', ', ': '), ensure_ascii=False)


def set_name():
    name = input("\033[32m* Enter dataset name: ")
    while name == '':
        name = input("\033[32m* Please enter dataset Name: ")
    return name


def set_creator():
    creator = input("\033[32mEnter creator name(Default value: " + get_user() + ") ")
    if creator == '':
        creator = get_user()
    return creator


def set_time():
    while True:
        date = input("\033[32mPlease enter create date(Default current datetime, format: %Y-%m-%d %H:%M:%S):")
        if date != '':
            try:
                datetime.datetime.strptime(date, timeFmt)
                return date
            except:
                continue
        else:
            return ''


def set_instruments():
    instruments = input("\033[32mEnter instrument name of data collector: ")
    return instruments


def set_format():
    dataType = input("\033[32m* Enter dataset format(img/array/binary/etc..):")
    while dataType == '':
        dataType = input("\033[32m* Enter dataset format(img/array/binary/etc..):")
    return dataType


def set_changes():
    changes = input("\033[32m* Enter changes: ")
    while changes == '':
        changes = input("\033[32m* Enter changes: ")
    return changes


def set_comments():
    comments = input("\033[32mSome comments? ")
    return comments


def set_raw_path():
    while True:
        rawPath = input('\033[31mPlease indicate the path of matched raw data: ')
        if rawPath[-1] != '/':
            rawPath = rawPath + '/'
        if check_path(rawPath):
            return rawPath
        else:
            print('Path not found! Please retry!')


def check_path(path):
    if os.path.exists(path + 'metadata.json'):
        return True
    else:
        return False


def set_new_value():
    value = input("\033[32mEnter new value: ")
    return value


def set_new_attributes():
    attrs = {}
    while True:
        newAttributes = input("\033[32mNew attributes to metadata(just enter json format string, 'q' to quit this "
                              "step)?")
        if newAttributes == 'q':
            break
        if newAttributes is not None and newAttributes != '':
            try:
                attr = json.loads(newAttributes)
                attrs.update(attr)
                print("\033[32mNew attributes: " + str(attrs))
            except Exception as e:
                print(e)
                print("\033[31mFormat exception in your inputs! Please retry!")
    return attrs


def create_metadata(path):
    oMeta = Metadata()
    if os.path.exists(path + 'metadata.json'):
        res = input('\033[31mThis is path already exists a metadata file, do you want to replace it?(y/n)')
        if res not in ['y', 'yes']:
            return
    oMeta.metadata['name'] = set_name()
    oMeta.metadata['creator'] = set_creator()
    date = set_time()
    if date != '':
        oMeta.metadata['createDate'] = date
    oMeta.metadata['instruments'] = set_instruments()
    oMeta.metadata['location'] = path
    oMeta.metadata['sets']['history'][0]['creator'] = oMeta.metadata['creator']
    oMeta.metadata['sets']['format'] = set_format()
    oMeta.metadata['comments'] = set_comments()
    oMeta.add_attributes(set_new_attributes())
    return oMeta


def create_train_metadata(path):
    oMeta = Metadata(dataType=DATATYPE['TRAIN_DATA'])
    rawPath = set_raw_path()
    rawMeta = load_metadata(rawPath)
    oMeta.merge_raw(rawMeta)
    oMeta.metadata['name'] = set_name()
    oMeta.metadata['creator'] = set_creator()
    date = set_time()
    if date != '':
        oMeta.metadata['createDate'] = date
    oMeta.metadata['location'] = path
    oMeta.metadata['sets']['history'][0]['creator'] = oMeta.metadata['creator']
    oMeta.metadata['sets']['format'] = set_format()
    oMeta.metadata['comments'] = set_comments()
    oMeta.add_attributes(set_new_attributes())
    return oMeta


def create_test_metadata(path):
    oMeta = Metadata(dataType=DATATYPE['TEST_DATA'])
    rawPath = set_raw_path()
    rawMeta = load_metadata(rawPath)
    oMeta.merge_raw(rawMeta)
    oMeta.metadata['name'] = set_name()
    oMeta.metadata['creator'] = set_creator()
    date = set_time()
    if date != '':
        oMeta.metadata['createDate'] = date
    oMeta.metadata['location'] = path
    oMeta.metadata['sets']['history'][0]['creator'] = oMeta.metadata['creator']
    oMeta.metadata['sets']['format'] = set_format()
    oMeta.metadata['comments'] = set_comments()
    oMeta.add_attributes(set_new_attributes())
    return oMeta


def copy_metadata(path, to):
    oMeta = Metadata()
    if os.path.exists(path + 'metadata.json'):
        origin = load_metadata(path)
        oMeta.load_metadata(origin, isCheck=False)
        if os.path.exists(to + 'metadata.json'):
            res = input('\033[31mThis is path already exists a metadata file, do you want to replace it?(y/n)')
            if res not in ['y', 'yes']:
                return
        oMeta.metadata['createDate'] = datetime.datetime.now().strftime(timeFmt)
        creator = set_creator()
        oMeta.metadata['location'] = to
        changes = 'COPY FROM ' + to + 'metadata.json'
        oMeta.add_history(creator, changes)
        return oMeta
    else:
        raise Exception("No folder named: " + path)


def copy_metadata_force(path, to):
    oMeta = Metadata()
    if os.path.exists(path + 'metadata.json'):
        origin = load_metadata(path)
        oMeta.load_metadata(origin, isCheck=False)
        oMeta.metadata['createDate'] = datetime.datetime.now().strftime(timeFmt)
        creator = oMeta.metadata['creator']
        oMeta.metadata['location'] = to
        changes = 'COPY FROM ' + to + 'metadata.json'
        oMeta.add_history(creator, changes)
        return oMeta
    else:
        raise Exception("No folder named: " + path)


def update_metadata(path):
    oMeta = Metadata()
    if os.path.exists(path + 'metadata.json'):
        origin = load_metadata(path)
        if origin.get('sets') and origin['sets'].get('type') and origin['sets']['type'] in DATATYPE:
            oMeta.load_metadata(origin, DATATYPE[origin['sets']['type']])
        else:
            raise Exception("metadata contains unknown datatype!")
        print(str(oMeta))
        print("Usage: history / attributes")
        print("Usage: creator/comments/name.. etc")
        print("Usage: sets type/origin/comments")
        while True:
            updateKey = input("\033[32mEnter the key you want to update(q to quit): ")
            if updateKey == 'history':
                creator = set_creator()
                changes = set_changes()
                oMeta.add_history(creator, changes)
            elif updateKey == 'attributes':
                oMeta.add_attributes(set_new_attributes())
            elif updateKey == 'creator':
                oMeta.metadata['creator'] = set_creator()
            elif updateKey[0:4] == 'sets':
                if updateKey[5:] == 'type':
                    print("You can not change this attribute!")
                if updateKey[5:] == 'comments':
                    oMeta.metadata['sets']['comments'] = set_comments()
                if updateKey[5:] == 'format':
                    oMeta.metadata['sets']['format'] = set_comments()
            elif updateKey == 'q':
                return oMeta
            elif oMeta.metadata.get(updateKey):
                oMeta.metadata[updateKey] = set_new_value()
            else:
                print("\033[31mError: No such Key! " + updateKey)
    else:
        raise Exception("No folder named: " + path)


def print_help():
    print("\033[32m--Usage: python3 metadata.py [update/show] path_to_metadata")
    print("\033[32m--Usage: python3 metadata.py [create] [raw/train/test] path_to_metadata")
    print("\033[32m--Usage: or python3 metadata.py [copy] [-f] path_source_metadata path_source_destination")
    print("\033[32m--For example: python3 metadata.py create raw ~/datasets/1/  生成源数据metadata引导")
    print("\033[32m--For example: python3 metadata.py create train ~/datasets/1/train/ 生成训练集metadata引导")
    print("\033[32m--For example: python3 metadata.py copy ~/datasets/1/, "
          "~/datasets/2/ 从~/datasets/1/复制一份metadata到~/datasets/2/")
    print("\033[32m--For example: python3 metadata.py copy -f ~/datasets/1/, "
          "~/datasets/2/ 从~/datasets/1/ 跳过引导步骤强制复制一份metadata到~/datasets/2/")


def save_metadata(path, oMeta):
    try:
        with open(path + 'metadata.json', 'w') as f:
            json.dump(oMeta.metadata, f)
        f.close()
        return f.name
    except Exception as e:
        raise e


def load_metadata(path):
    try:
        with open(path + 'metadata.json', 'r') as f:
            meta = json.load(f)
        f.close()
        return meta
    except Exception as e:
        raise e


if __name__ == '__main__':
    args = sys.argv

    if len(args) == 2 and args[1] in ['-h', 'help']:
        print_help()
        exit(0)

    if args[1] not in actions:
        print_help()
        print("\033[31mError: action: %s is unexpected! The program will exit!", args[1])
        exit(1)

    if args[2] != '-f' and not os.path.exists(args[-1]):
        print_help()
        print("\033[31mError: path: " + args[-1] + "doesn't exist! The program will exit!")
        exit(1)

    if args[1] == 'create':
        if not args[-1][-1] == '/':
            args[-1] = args[-1] + '/'
        try:
            if not args[2] in dtypes:
                print_help()
                print("\033[31mError: types not in [raw/train/test]")
                exit(1)
            if args[2] == 'test':
                metaObj = create_train_metadata(args[-1])
            elif args[2] == 'train':
                metaObj = create_train_metadata(args[-1])
            else:
                metaObj = create_metadata(args[-1])
            print()
            print("=============================================================================================")
            print(str(metaObj))
            name = save_metadata(args[-1], metaObj)
            print("\033[36mMetadata created and saved at " + name)
            print("==============================================================================================")
        except Exception as e:
            print_help()
            print(e)
            print("The program will exit!")
            exit(1)

    if args[1] == 'show':
        if not args[2][-1] == '/':
            args[2] = args[2] + '/'
        try:
            loads = load_metadata(args[2])
            metadata = Metadata()
            metadata.metadata = loads
            print(str(metadata))
        except Exception as e:
            print_help()
            print("\033[31mError:" + str(e))
            print("The program will exit!")
            exit(1)

    if args[1] == 'copy':
        try:
            if len(args) < 4:
                print_help()
                print("The program will exit!")
                exit(1)
            if args[-2][-1] != '/':
                args[-2] = args[-2] + '/'
            if not args[-1][-1] == '/':
                args[-1] = args[-1] + '/'
            if len(args) == 5 and args[2] == '-f':
                metaObj = copy_metadata_force(args[3], args[4])
            else:
                metaObj = copy_metadata(args[2], args[3])
            print()
            print("=============================================================================================")
            print(str(metaObj))
            name = save_metadata(args[-1], metaObj)
            print("\033[36mMetadata created and saved at " + name)
            print("==============================================================================================")
        except Exception as e:
            print_help()
            print("\033[31mError:" + str(e))
            print("The program will exit!")
            exit(1)

    if args[1] == 'update':
        if not args[2][-1] == '/':
            args[2] = args[2] + '/'
        try:
            metaObj = update_metadata(args[2])
            print()
            print("=============================================================================================")
            print(str(metaObj))
            name = save_metadata(args[-1], metaObj)
            print("\033[36mMetadata updated and saved at " + name)
            print("==============================================================================================")
        except Exception as e:
            print_help()
            print("\033[31mError:" + str(e))
            print("The program will exit!")
            exit(1)
    print("Bye")
    exit(0)
